package study.alex.resumedb.gae;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.appengine.datastore.AppEngineDataStoreFactory;
import com.google.api.client.extensions.appengine.http.UrlFetchTransport;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Userinfo;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.server.*;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created with IntelliJ IDEA.
 * User: alex
 * Date: 13.01.14
 * Time: 12:57
 * SVN $Id$
 */

@WebServlet(value = {"/app/*", "/VAADIN/*"}, asyncSupported = true)
@VaadinServletConfiguration(productionMode = false, ui = ResumeDBMaintUI.class, widgetset = "study.alex.resumedb.gae.AppWidgetSet")
public class ResumeDbMainServlet extends GAEVaadinServlet
        implements SessionInitListener, SessionDestroyListener {

    private final static Logger log = Logger.getLogger(ResumeDbMainServlet.class.getName());

    /**
     * Default transportation layer for Google Apis Java client.
     */
    protected static final HttpTransport TRANSPORT = new UrlFetchTransport();

    /**
     * Default JSON factory for Google Apis Java client.
     */
    protected static final JsonFactory JSON_FACTORY = new JacksonFactory();

    /**
     * Dafault Datastore factory for store credentials
     */
    protected static final AppEngineDataStoreFactory DATA_STORE_FACTORY = AppEngineDataStoreFactory.getDefaultInstance();

    /**
     * Path component under war/ to locate client_secrets.json file.
     */
    public static final String CLIENT_SECRETS_FILE_PATH
            = "/WEB-INF/client_secret.json";

    /**
     * A credential manager to get, set, delete credential objects.
     */
    private CredentialManager credentialManager = null;

    /**
     * Initializes the Servlet.
     */
    @Override
    public void init() throws ServletException {
        super.init();
        // init credential manager
        try {
            credentialManager = new CredentialManager(getClientSecrets(), TRANSPORT, JSON_FACTORY, DATA_STORE_FACTORY);
        } catch (IOException e) {
            log.severe("Can't make datastore for credentials");
            throw new ServletException(e);
        }
    }
    @Override
    protected void servletInitialized() throws ServletException {
        super.servletInitialized();
        getService().addSessionInitListener(this);
        getService().addSessionDestroyListener(this);
        log.info("Servlet initialized. Current user is " + UserServiceFactory.getUserService().getCurrentUser());

    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {

        StringBuffer url = request.getRequestURL();
        log.fine("Request url: " + url.toString());
        if(request.getScheme().equals("http"))
        {
            String newUrl = url.replace(0, 4, "https").toString();
            resp.sendRedirect(newUrl);
            return;
        }

        // handle OAuth2 callback
        if (handleCallbackIfRequired(request, resp)) return;

        String uri = request.getRequestURI();
        if (uri.contains("VAADIN") || uri.contains("HEARTBEAT") || uri.contains("CLEAN") || uri.contains("UIDL")
                || uri.contains("UPLOAD") || uri.contains("APP") || uri.contains("PUSH")) {
            super.service(request, resp);
            return;
        }

/*
        User user = UserServiceFactory.getUserService().getCurrentUser();
        if (user == null) {
            log.info("Google login requires.  Is user logged in? - " + UserServiceFactory.getUserService().isUserLoggedIn());
            resp.sendRedirect("/login-required.html");
            return;
        }
*/

        // Making sure that we have user credentials
        if (loginIfRequired(request, resp)) return;

        super.service(request, resp);
    }

    private void denyGoogleDrive(String userId) {
        try {
            if (userId == null || userId.isEmpty()) throw new IOException("Null userId");
            credentialManager.saveStatus(userId, CredentialManager.GoogleDiskAccessStatus.DENY);
        } catch (IOException e) {
            log.severe("Can't save Google Drive permission\n" + e);
        }
    }

    @Override
    public void sessionInit(SessionInitEvent event)
            throws ServiceException {
        log.info("New session init");
    }

    @Override
    public void sessionDestroy(SessionDestroyEvent event) {
        log.info("Session destroy");
    }

    /**
     * Build and return an Oauth2 service object based on given request parameters.
     * @param credential User credentials.
     * @return Drive service object that is ready to make requests, or null if
     *         there was a problem.
     */
    protected Oauth2 getOauth2Service(Credential credential) {
        Oauth2 service = new Oauth2.Builder(TRANSPORT, JSON_FACTORY, credential)
                .setApplicationName("ResumeDB WS").build();
        return service;
    }

    /**
     * Reads client_secrets.json and creates a GoogleClientSecrets object.
     * @return A GoogleClientsSecrets object.
     */
    private GoogleClientSecrets getClientSecrets() {


        ServletContext context = getServletContext();
        InputStream stream =
                context.getResourceAsStream(CLIENT_SECRETS_FILE_PATH);
        try {
            return GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(stream));
        } catch (IOException e) {
            throw new RuntimeException("No client_secrets.json found");
        }
    }

    protected boolean isNeedAuthorization() {
        User user = UserServiceFactory.getUserService().getCurrentUser();
        if (user == null) {
            log.info("Google login requires.  Is user logged in? - " + UserServiceFactory.getUserService().isUserLoggedIn());
            return true;
        }

        String userId = user.getUserId();
        try {
            CredentialManager.GoogleDiskAccessStatus status = credentialManager.getStatus(userId);

            return (status == CredentialManager.GoogleDiskAccessStatus.UNKNOWN)
                    || (status == CredentialManager.GoogleDiskAccessStatus.GRANT && credentialManager.get(userId) == null);
        } catch (IOException e) {
            log.log(Level.SEVERE, "Can't get info from store.  UserId is " + userId, e);
            return false;
        }
    }

    /**
     * Returns the credentials of the user in the session. If user is not in the
     * session, returns null.
     * @return      Credential object of the user in session or null.
     */
    public Credential getCredential() {
        User user = UserServiceFactory.getUserService().getCurrentUser();
        if (user == null) return null;

        String userId = user.getUserId();

        log.fine("Id from UserService is " + userId);

        if (userId != null) {
            try {
                return credentialManager.get(userId);
            } catch (IOException e) {
                log.log(Level.SEVERE, "Can't get credential from store.  UserId is " + userId, e);
                return null;
            }
        }
        return null;
    }

    /**
     * Clears credential in store for current user and returns it. If user does not have the
     * credential.
     */

    public void dropCredential() {
        User user = UserServiceFactory.getUserService().getCurrentUser();
        if (user == null) return;

        String userId = user.getUserId();

        log.fine("Drop credential for userId " + userId);

        if (userId != null) {
            try {
                credentialManager.drop(userId);
            } catch (IOException e) {
                log.log(Level.SEVERE, "Can't drop credential from store.  UserId is " + userId, e);
            }
        }
    }

    public CredentialManager getCredentialManager() {
        return credentialManager;
    }

    /**
     * Redirects to OAuth2 consent page if user is not logged in.
     * @param req   Request object.
     * @param resp  Response object.
     */
    protected boolean loginIfRequired(HttpServletRequest req,
                                   HttpServletResponse resp) {
        if (isNeedAuthorization()) {

            // redirect to authorization url
            try {
                User user = UserServiceFactory.getUserService().getCurrentUser();
                String userId;
                if (user != null) userId = user.getUserId();
                else {
                    log.info("Google login requires.  Is user logged in? - " + UserServiceFactory.getUserService().isUserLoggedIn());
                    resp.sendRedirect("/login-required.html");
                    return true;
                }
                log.info("Going to OAuth2 page for user " + userId);
                resp.sendRedirect(credentialManager.getAuthorizationUrl(userId));
                return true;
            } catch (IOException e) {
                throw new RuntimeException("Can't redirect to auth page");
            }
        }
        return false;
    }

    /**
     * If OAuth2 redirect callback is invoked and there is a code query param,
     * retrieve user credentials and profile. Then, redirect to the home page.
     * @param req   Request object.
     * @param resp  Response object.
     * @throws IOException
     */
    protected boolean handleCallbackIfRequired(HttpServletRequest req,
                                            HttpServletResponse resp) throws IOException {

        User user = UserServiceFactory.getUserService().getCurrentUser();
        String errorState = req.getParameter("error");
        String state = req.getParameter("state");

        if (errorState != null && errorState.equalsIgnoreCase("access_denied")) {
            log.info("Received error state request");
            if (user != null) {
                denyGoogleDrive(user.getUserId());
                resp.sendRedirect("/app");
                return true;
            } else {
                log.info("Got error status but user for session not defined.  Remote user is " + req.getRemoteUser());
                Cookie[] cookies = req.getCookies();
                log.info("Cookies in request:");
                for (Cookie c : cookies) {
                    log.info("Name: " + c.getName() + " Domain: " + c.getDomain() + " Value: " + c.getValue());
                }
                if (state != null && !state.isEmpty()) {
                    log.info("Got userId from state.  UserId is " + state);
                    denyGoogleDrive(state);
                    resp.sendRedirect("/app");
                }
                else resp.sendRedirect("/cookies-required.html");
                return true;
            }
        }

        String code = req.getParameter("code");
        if (code != null) {
            // retrieve new credentials with code
            log.info("Exchange auth code " + code);
            Credential credential = credentialManager.retrieve(code);

            String userId;

            if (user == null) {
                log.info("Got auth code but user for session not defined.  Remote user is " + req.getRemoteUser());
                Cookie[] cookies = req.getCookies();
                log.info("Cookies in request:");
                for (Cookie c : cookies) {
                    log.info("Name: " + c.getName() + " Domain: " + c.getDomain() + " Value: " + c.getValue());
                }
                if (state != null && !state.isEmpty()) {
                    log.info("Got userId from state.  UserId is " + state);
                    userId = state;
                }
                else {
                    resp.sendRedirect("/cookies-required.html");
                    return true;
                }
            } else userId = user.getUserId();

            if (credential != null) {

                // request userinfo
                Oauth2 service = getOauth2Service(credential);
                try {
                    Userinfo about = service.userinfo().get().execute();
                    String id = about.getId();

                    log.info("Id from About service is " + id);

                    //Save credential at user's User API key, not userId from About info

                    credentialManager.save(userId, credential);
                } catch (IOException e) {
                    log.log(Level.SEVERE, "Exception when get about info or save credential", e);
                    throw new RuntimeException("Can't handle the OAuth2 callback, " +
                            "make sure that code is valid.");
                }
            } else {
                denyGoogleDrive(userId);
            }
            resp.sendRedirect("/app");
            return true;
        }
        return false;
    }
}